/**
 * The following features are still outstanding: animation as a
 * function, placement as a function, inside, support for more triggers than
 * just mouse enter/leave, html tooltips, and selector delegation.
 */


/**
 * The $tooltip service creates tooltip- and popover-like directives as well as
 * houses global options for them.
 */
as.provider('$aTooltip', function () {
        // The default options tooltip and popover.
        var defaultOptions = {
            placement           : 'top',
            placementClassPrefix: '',
            animation           : true,
            popupDelay          : 0,
            popupCloseDelay     : 0,
            useContentExp       : false
        };

        // Default hide triggers for each show trigger
        var triggerMap = {
            'mouseenter'  : 'mouseleave',
            'click'       : 'click',
            'outsideClick': 'outsideClick',
            'focus'       : 'blur',
            'none'        : ''
        };

        // The options specified to the provider globally.
        var globalOptions = {};

        /**
         * `options({})` allows global configuration of all tooltips in the
         * application.
         *
         *   var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) {
   *     // place tooltips left instead of top by default
   *     $tooltipProvider.options( { placement: 'left' } );
   *   });
         */
        this.options = function (value) {
            angular.extend(globalOptions, value);
        };

        /**
         * This allows you to extend the set of trigger mappings available. E.g.:
         *
         *   $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' );
         */
        this.setTriggers = function setTriggers(triggers) {
            angular.extend(triggerMap, triggers);
        };

        /**
         * This is a helper function for translating camel-case to snake_case.
         */
        function snake_case(name) {
            var regexp = /[A-Z]/g;
            var separator = '-';
            return name.replace(regexp, function (letter, pos) {
                return (pos ? separator : '') + letter.toLowerCase();
            });
        }

        /**
         * Returns the actual instance of the $tooltip service.
         * TODO support multiple triggers
         */
        this.$get = ['$window', '$compile', '$timeout', '$document', '$$position', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function ($window, $compile, $timeout, $document, $$position, $interpolate, $rootScope, $parse, $$stackedMap) {
            var openedTooltips = $$stackedMap.createNew();
            $document.on('keypress', keypressListener);

            $rootScope.$on('$destroy', function () {
                $document.off('keypress', keypressListener);
            });

            function keypressListener(e) {
                if (e.which === 27) {
                    var last = openedTooltips.top();
                    if (last) {
                        last.value.close();
                        openedTooltips.removeTop();
                        last = null;
                    }
                }
            }

            return function $tooltip(ttType, prefix, defaultTriggerShow, options) {
                options = angular.extend({}, defaultOptions, globalOptions, options);

                /**
                 * Returns an object of show and hide triggers.
                 *
                 * If a trigger is supplied,
                 * it is used to show the tooltip; otherwise, it will use the `trigger`
                 * option passed to the `$tooltipProvider.options` method; else it will
                 * default to the trigger supplied to this directive factory.
                 *
                 * The hide trigger is based on the show trigger. If the `trigger` option
                 * was passed to the `$tooltipProvider.options` method, it will use the
                 * mapped trigger from `triggerMap` or the passed trigger if the map is
                 * undefined; otherwise, it uses the `triggerMap` value of the show
                 * trigger; else it will just use the show trigger.
                 */
                function getTriggers(trigger) {
                    var show = (trigger || options.trigger || defaultTriggerShow).split(' ');
                    var hide = show.map(function (trigger) {
                        return triggerMap[trigger] || trigger;
                    });
                    return {
                        show: show,
                        hide: hide
                    };
                }

                var directiveName = snake_case(ttType);

                var startSym = $interpolate.startSymbol();
                var endSym = $interpolate.endSymbol();
                var template =
                        '<div ' + directiveName + '-popup ' +
                        'title="' + startSym + 'title' + endSym + '" ' +
                        (options.useContentExp ?
                            'content-exp="contentExp()" ' :
                        'content="' + startSym + 'content' + endSym + '" ') +
                        'placement="' + startSym + 'placement' + endSym + '" ' +
                        'popup-class="' + startSym + 'popupClass' + endSym + '" ' +
                        'animation="animation" ' +
                        'is-open="isOpen"' +
                        'origin-scope="origScope" ' +
                        'style="visibility: hidden; display: block; top: -9999px; left: -9999px;"' +
                        '>' +
                        '</div>';

                return {
                    compile: function (tElem, tAttrs) {
                        var tooltipLinker = $compile(template);

                        return function link(scope, element, attrs, tooltipCtrl) {
                            var tooltip;
                            var tooltipLinkedScope;
                            var transitionTimeout;
                            var showTimeout;
                            var hideTimeout;
                            var positionTimeout;
                            var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
                            var triggers = getTriggers(undefined);
                            var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
                            var ttScope = scope.$new(true);
                            var repositionScheduled = false;
                            var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
                            var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false;
                            var observers = [];

                            var positionTooltip = function () {
                                // check if tooltip exists and is not empty
                                if (!tooltip || !tooltip.html()) {
                                    return;
                                }

                                if (!positionTimeout) {
                                    positionTimeout = $timeout(function () {
                                        // Reset the positioning.
                                        tooltip.css({top: 0, left: 0});

                                        // Now set the calculated positioning.
                                        var ttPosition = $$position.positionElements(element, tooltip, ttScope.placement, appendToBody);
                                        tooltip.css({top: ttPosition.top + 'px', left: ttPosition.left + 'px', visibility: 'visible'});

                                        // If the placement class is prefixed, still need
                                        // to remove the TWBS standard class.
                                        if (options.placementClassPrefix) {
                                            tooltip.removeClass('top bottom left right');
                                        }

                                        tooltip.removeClass(
                                            options.placementClassPrefix + 'top ' +
                                            options.placementClassPrefix + 'top-left ' +
                                            options.placementClassPrefix + 'top-right ' +
                                            options.placementClassPrefix + 'bottom ' +
                                            options.placementClassPrefix + 'bottom-left ' +
                                            options.placementClassPrefix + 'bottom-right ' +
                                            options.placementClassPrefix + 'left ' +
                                            options.placementClassPrefix + 'left-top ' +
                                            options.placementClassPrefix + 'left-bottom ' +
                                            options.placementClassPrefix + 'right ' +
                                            options.placementClassPrefix + 'right-top ' +
                                            options.placementClassPrefix + 'right-bottom');

                                        var placement = ttPosition.placement.split('-');
                                        tooltip.addClass(placement[0] + ' ' + options.placementClassPrefix + ttPosition.placement);
                                        $$position.positionArrow(tooltip, ttPosition.placement);

                                        positionTimeout = null;
                                    }, 0, false);
                                }
                            };

                            // Set up the correct scope to allow transclusion later
                            ttScope.origScope = scope;

                            // By default, the tooltip is not open.
                            // TODO add ability to start tooltip opened
                            ttScope.isOpen = false;
                            openedTooltips.add(ttScope, {
                                close: hide
                            });

                            function toggleTooltipBind() {
                                if (!ttScope.isOpen) {
                                    showTooltipBind();
                                } else {
                                    hideTooltipBind();
                                }
                            }

                            // Show the tooltip with delay if specified, otherwise show it immediately
                            function showTooltipBind() {
                                if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) {
                                    return;
                                }

                                cancelHide();
                                prepareTooltip();

                                if (ttScope.popupDelay) {
                                    // Do nothing if the tooltip was already scheduled to pop-up.
                                    // This happens if show is triggered multiple times before any hide is triggered.
                                    if (!showTimeout) {
                                        showTimeout = $timeout(show, ttScope.popupDelay, false);
                                    }
                                } else {
                                    show();
                                }
                            }

                            function hideTooltipBind() {
                                cancelShow();

                                if (ttScope.popupCloseDelay) {
                                    if (!hideTimeout) {
                                        hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false);
                                    }
                                } else {
                                    hide();
                                }
                            }

                            // Show the tooltip popup element.
                            function show() {
                                cancelShow();
                                cancelHide();

                                // Don't show empty tooltips.
                                if (!ttScope.content) {
                                    return angular.noop;
                                }

                                createTooltip();

                                // And show the tooltip.
                                ttScope.$evalAsync(function () {
                                    ttScope.isOpen = true;
                                    assignIsOpen(true);
                                    positionTooltip();
                                });
                            }

                            function cancelShow() {
                                if (showTimeout) {
                                    $timeout.cancel(showTimeout);
                                    showTimeout = null;
                                }

                                if (positionTimeout) {
                                    $timeout.cancel(positionTimeout);
                                    positionTimeout = null;
                                }
                            }

                            // Hide the tooltip popup element.
                            function hide() {
                                if (!ttScope) {
                                    return;
                                }

                                // First things first: we don't show it anymore.
                                ttScope.$evalAsync(function () {
                                    if (ttScope) {
                                        ttScope.isOpen = false;
                                        assignIsOpen(false);
                                        // And now we remove it from the DOM. However, if we have animation, we
                                        // need to wait for it to expire beforehand.
                                        // FIXME: this is a placeholder for a port of the transitions library.
                                        // The fade transition in TWBS is 150ms.
                                        if (ttScope.animation) {
                                            if (!transitionTimeout) {
                                                transitionTimeout = $timeout(removeTooltip, 150, false);
                                            }
                                        } else {
                                            removeTooltip();
                                        }
                                    }
                                });
                            }

                            function cancelHide() {
                                if (hideTimeout) {
                                    $timeout.cancel(hideTimeout);
                                    hideTimeout = null;
                                }

                                if (transitionTimeout) {
                                    $timeout.cancel(transitionTimeout);
                                    transitionTimeout = null;
                                }
                            }

                            function createTooltip() {
                                // There can only be one tooltip element per directive shown at once.
                                if (tooltip) {
                                    return;
                                }

                                tooltipLinkedScope = ttScope.$new();
                                tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) {
                                    if (appendToBody) {
                                        $document.find('body').append(tooltip);
                                    } else {
                                        element.after(tooltip);
                                    }
                                });

                                prepObservers();
                            }

                            function removeTooltip() {
                                cancelShow();
                                cancelHide();
                                unregisterObservers();

                                if (tooltip) {
                                    tooltip.remove();
                                    tooltip = null;
                                }
                                if (tooltipLinkedScope) {
                                    tooltipLinkedScope.$destroy();
                                    tooltipLinkedScope = null;
                                }
                            }

                            /**
                             * Set the initial scope values. Once
                             * the tooltip is created, the observers
                             * will be added to keep things in sync.
                             */
                            function prepareTooltip() {
                                ttScope.title = attrs[prefix + 'Title'];
                                if (contentParse) {
                                    ttScope.content = contentParse(scope);
                                } else {
                                    ttScope.content = attrs[ttType];
                                }

                                ttScope.popupClass = attrs[prefix + 'Class'];
                                ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement;

                                var delay = parseInt(attrs[prefix + 'PopupDelay'], 10);
                                var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10);
                                ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
                                ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay;
                            }

                            function assignIsOpen(isOpen) {
                                if (isOpenParse && angular.isFunction(isOpenParse.assign)) {
                                    isOpenParse.assign(scope, isOpen);
                                }
                            }

                            ttScope.contentExp = function () {
                                return ttScope.content;
                            };

                            /**
                             * Observe the relevant attributes.
                             */
                            attrs.$observe('disabled', function (val) {
                                if (val) {
                                    cancelShow();
                                }

                                if (val && ttScope.isOpen) {
                                    hide();
                                }
                            });

                            if (isOpenParse) {
                                scope.$watch(isOpenParse, function (val) {
                                    if (ttScope && !val === ttScope.isOpen) {
                                        toggleTooltipBind();
                                    }
                                });
                            }

                            function prepObservers() {
                                observers.length = 0;

                                if (contentParse) {
                                    observers.push(
                                        scope.$watch(contentParse, function (val) {
                                            ttScope.content = val;
                                            if (!val && ttScope.isOpen) {
                                                hide();
                                            }
                                        })
                                    );

                                    observers.push(
                                        tooltipLinkedScope.$watch(function () {
                                            if (!repositionScheduled) {
                                                repositionScheduled = true;
                                                tooltipLinkedScope.$$postDigest(function () {
                                                    repositionScheduled = false;
                                                    if (ttScope && ttScope.isOpen) {
                                                        positionTooltip();
                                                    }
                                                });
                                            }
                                        })
                                    );
                                } else {
                                    observers.push(
                                        attrs.$observe(ttType, function (val) {
                                            ttScope.content = val;
                                            if (!val && ttScope.isOpen) {
                                                hide();
                                            } else {
                                                positionTooltip();
                                            }
                                        })
                                    );
                                }

                                observers.push(
                                    attrs.$observe(prefix + 'Title', function (val) {
                                        ttScope.title = val;
                                        if (ttScope.isOpen) {
                                            positionTooltip();
                                        }
                                    })
                                );

                                observers.push(
                                    attrs.$observe(prefix + 'Placement', function (val) {
                                        ttScope.placement = val ? val : options.placement;
                                        if (ttScope.isOpen) {
                                            positionTooltip();
                                        }
                                    })
                                );
                            }

                            function unregisterObservers() {
                                if (observers.length) {
                                    angular.forEach(observers, function (observer) {
                                        observer();
                                    });
                                    observers.length = 0;
                                }
                            }

                            // hide tooltips/popovers for outsideClick trigger
                            function bodyHideTooltipBind(e) {
                                if (!ttScope || !ttScope.isOpen || !tooltip) {
                                    return;
                                }
                                // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked
                                if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) {
                                    hideTooltipBind();
                                }
                            }

                            var unregisterTriggers = function () {
                                triggers.show.forEach(function (trigger) {
                                    if (trigger === 'outsideClick') {
                                        element.off('click', toggleTooltipBind);
                                    } else {
                                        element.off(trigger, showTooltipBind);
                                        element.off(trigger, toggleTooltipBind);
                                    }
                                });
                                triggers.hide.forEach(function (trigger) {
                                    if (trigger === 'outsideClick') {
                                        $document.off('click', bodyHideTooltipBind);
                                    } else {
                                        element.off(trigger, hideTooltipBind);
                                    }
                                });
                            };

                            function prepTriggers() {
                                var val = attrs[prefix + 'Trigger'];
                                unregisterTriggers();

                                triggers = getTriggers(val);

                                if (triggers.show !== 'none') {
                                    triggers.show.forEach(function (trigger, idx) {
                                        if (trigger === 'outsideClick') {
                                            element.on('click', toggleTooltipBind);
                                            $document.on('click', bodyHideTooltipBind);
                                        } else if (trigger === triggers.hide[idx]) {
                                            element.on(trigger, toggleTooltipBind);
                                        } else if (trigger) {
                                            element.on(trigger, showTooltipBind);
                                            element.on(triggers.hide[idx], hideTooltipBind);
                                        }

                                        element.on('keypress', function (e) {
                                            if (e.which === 27) {
                                                hideTooltipBind();
                                            }
                                        });
                                    });
                                }
                            }

                            prepTriggers();

                            var animation = scope.$eval(attrs[prefix + 'Animation']);
                            ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;

                            var appendToBodyVal;
                            var appendKey = prefix + 'AppendToBody';
                            if (appendKey in attrs && attrs[appendKey] === undefined) {
                                appendToBodyVal = true;
                            } else {
                                appendToBodyVal = scope.$eval(attrs[appendKey]);
                            }

                            appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;

                            // Make sure tooltip is destroyed and removed.
                            scope.$on('$destroy', function onDestroyTooltip() {
                                unregisterTriggers();
                                removeTooltip();
                                openedTooltips.remove(ttScope);
                                ttScope = null;
                            });
                        };
                    }
                };
            };
        }];
    })

    // This is mostly ngInclude code but with a custom scope
    .directive('aTooltipTemplateTransclude', [
        '$animate', '$sce', '$compile', '$templateRequest',
        function ($animate, $sce, $compile, $templateRequest) {
            return {
                link: function (scope, elem, attrs) {
                    var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);

                    var changeCounter = 0,
                        currentScope,
                        previousElement,
                        currentElement;

                    var cleanupLastIncludeContent = function () {
                        if (previousElement) {
                            previousElement.remove();
                            previousElement = null;
                        }

                        if (currentScope) {
                            currentScope.$destroy();
                            currentScope = null;
                        }

                        if (currentElement) {
                            $animate.leave(currentElement).then(function () {
                                previousElement = null;
                            });
                            previousElement = currentElement;
                            currentElement = null;
                        }
                    };

                    scope.$watch($sce.parseAsResourceUrl(attrs.aTooltipTemplateTransclude), function (src) {
                        var thisChangeId = ++changeCounter;

                        if (src) {
                            //set the 2nd param to true to ignore the template request error so that the inner
                            //contents and scope can be cleaned up.
                            $templateRequest(src, true).then(function (response) {
                                if (thisChangeId !== changeCounter) {
                                    return;
                                }
                                var newScope = origScope.$new();
                                var template = response;

                                var clone = $compile(template)(newScope, function (clone) {
                                    cleanupLastIncludeContent();
                                    $animate.enter(clone, elem);
                                });

                                currentScope = newScope;
                                currentElement = clone;

                                currentScope.$emit('$includeContentLoaded', src);
                            }, function () {
                                if (thisChangeId === changeCounter) {
                                    cleanupLastIncludeContent();
                                    scope.$emit('$includeContentError', src);
                                }
                            });
                            scope.$emit('$includeContentRequested', src);
                        } else {
                            cleanupLastIncludeContent();
                        }
                    });

                    scope.$on('$destroy', cleanupLastIncludeContent);
                }
            };
        }])

    /**
     * Note that it's intentional that these classes are *not* applied through $animate.
     * They must not be animated as they're expected to be present on the tooltip on
     * initialization.
     */
    .directive('aTooltipClasses', ['$$position', function ($$position) {
        return {
            restrict: 'A',
            link    : function (scope, element, attrs) {
                // need to set the primary position so the
                // arrow has space during position measure.
                // tooltip.positionTooltip()
                if (scope.placement) {
                    // // There are no top-left etc... classes
                    // // in TWBS, so we need the primary position.
                    var position = $$position.parsePlacement(scope.placement);
                    element.addClass(position[0]);
                } else {
                    element.addClass('top');
                }

                if (scope.popupClass) {
                    element.addClass(scope.popupClass);
                }

                if (scope.animation()) {
                    element.addClass(attrs.tooltipAnimationClass);
                }
            }
        };
    }])

    .directive('aTooltipPopup', function () {
        return {
            replace : true,
            scope   : {content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&'},
            template: function () {
                var content = [];
                content.push('<div class="tooltip" tooltip-animation-class="fade" a-tooltip-classes ng-class="{ in: isOpen() }">');
                content.push('  <div class="tooltip-arrow"></div>');
                content.push('  <div class="tooltip-inner" ng-bind="content"></div>');
                content.push('</div>');
                return content.join('');
            }
        };
    })

    .directive('aTooltip', ['$aTooltip', function ($aTooltip) {
        return $aTooltip('aTooltip', 'tooltip', 'mouseenter');
    }])

    .directive('aTooltipTemplatePopup', function () {
        return {
            replace : true,
            scope   : {
                contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
                originScope                                                             : '&'
            },
            template: function () {
                var content = [];
                content.push('<div class="tooltip" tooltip-animation-class="fade" a-tooltip-classes ng-class="{ in: isOpen() }">');
                content.push('  <div class="tooltip-arrow"></div>');
                content.push('  <div class="tooltip-inner" a-tooltip-template-transclude="contentExp()" tooltip-template-transclude-scope="originScope()"></div>');
                content.push('</div>');
                return content.join('');
            }
        };
    })

    .directive('aTooltipTemplate', ['$aTooltip', function ($aTooltip) {
        return $aTooltip('aTooltipTemplate', 'tooltip', 'mouseenter', {
            useContentExp: true
        });
    }])

    .directive('aTooltipHtmlPopup', function () {
        return {
            replace : true,
            scope   : {contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&'},
            template: function () {
                var content = [];
                content.push('<div class="tooltip" tooltip-animation-class="fade" a-tooltip-classes ng-class="{ in: isOpen() }">');
                content.push('  <div class="tooltip-arrow"></div>');
                content.push('  <div class="tooltip-inner" ng-bind-html="contentExp()"></div>');
                content.push('</div>');
                return content.join('');
            }
        };
    })

    .directive('aTooltipHtml', ['$aTooltip', function ($aTooltip) {
        return $aTooltip('aTooltipHtml', 'tooltip', 'mouseenter', {
            useContentExp: true
        });
    }]);
